Skip to content

feat: Add remaining getting-started snippets.#398

Draft
kinyoklion wants to merge 20 commits intorlamb/sdk-snippetsfrom
rlamb/port-remaining-getting-started
Draft

feat: Add remaining getting-started snippets.#398
kinyoklion wants to merge 20 commits intorlamb/sdk-snippetsfrom
rlamb/port-remaining-getting-started

Conversation

@kinyoklion
Copy link
Copy Markdown
Member

No description provided.

@kinyoklion kinyoklion changed the title feat: Add server snippets. feat: Add remaining getting-started snippets. Apr 27, 2026
@kinyoklion kinyoklion force-pushed the rlamb/port-remaining-getting-started branch from b24d0d0 to 4ea69f2 Compare April 28, 2026 16:13
@kinyoklion kinyoklion changed the base branch from main to rlamb/sdk-snippets April 28, 2026 17:38
Adds snippet content for every SDK in gonfalon/static/ld/components/
getStarted/sdk/ outside of python-server-sdk (which landed in the
prior slice). Each SDK has a `sdk.yaml` descriptor and one
`<name>.snippet.md` per gonfalon `<Snippet>` block.

Tier 1 — server-side / Linux Docker (validators in this PR):
  go-server-sdk, node-server-sdk, node-client-sdk, ruby-server-sdk,
  php-server-sdk, rust-server-sdk, dotnet-server-sdk,
  dotnet-client-sdk, java-server-sdk

Tier 1 — server-side, validator pending:
  haskell-server-sdk, erlang-server-sdk, lua-server-sdk,
  cpp-server-sdk, cpp-client-sdk

Tier 2 — browser:
  js-client-sdk (Playwright validator in this PR), vue-client-sdk
  (validator pending), react-client-sdk (legacy + createApp variants;
  legacy renders into gonfalon, createApp deferred until its
  assetSource pattern is migrated)

Tier 3 — mobile/native (validators all pending):
  android-client-sdk, flutter-client-sdk, react-native-client-sdk

Tier 4 — iOS:
  ios-client-sdk (validator pending — needs macos-* runner)

Skipped:
  roku-client-sdk (validation: none; manual procedure documented in
  frontmatter)

Each SDK with markers in gonfalon was verified: `snippets render`
idempotent, `snippets verify` ok. The 11 SDKs with validators in
this PR were exercised end-to-end against the real LD test
environment locally. Three "fix on red" surfaces baked in:
  - node-client-sdk + dotnet-client-sdk: rewrote the print line so
    the snippet emits the EXAM-HELLO canonical phrase
    `feature flag evaluates to true` instead of `Feature flag X is
    true`. Snippets matched the gonfalon source verbatim before
    the fix; validators surfaced the divergence.
  - java-server-sdk: gonfalon's stale `5.0.0` version fallback
    swapped for `${version}` only; the validator's synthesized pom
    pins 7.13.4 (current Maven Central).

Snippets that contain non-EXAM-HELLO output today (e.g. cpp's
`Feature flag '<key>' is true`) carry their content verbatim with a
`# Validator pending` comment in frontmatter; the snippet is
rendered into gonfalon today and the canonical-line fix follows
when the per-SDK validator lands.
Each `validators/languages/<runtime>/` carries a `runner.yaml`
declaring `mode: docker`, an `image-prefix:` for the per-validator
content-hash tag, and a `runs-on:` hint. The Dockerfile builds the
language image (with the shared lib at `/harness-shared` per the
build-context refactor in the parent branch) and the harness sources
`/harness-shared/lib.sh` for the polling/timeout loop and key-
redacting log dump.

Runtimes added:
  - go              → go-server-sdk
  - node            → node-server-sdk + node-client-sdk
  - ruby            → ruby-server-sdk (with $stdout.sync = true shim
                      so block-buffered output reaches the matcher)
  - php             → php-server-sdk (composer pre-installed)
  - dotnet-server   → dotnet-server-sdk (synthesizes a minimal .csproj)
  - dotnet-client   → dotnet-client-sdk (top-level-statements .csproj)
  - jvm             → java-server-sdk (synthesizes pom.xml pinning
                      LD java 7.13.4 + maven-assembly-plugin; harness
                      prepends `package com.launchdarkly.tutorial;`
                      to App.java since `mvn archetype:generate`
                      writes that line for users)
  - rust            → rust-server-sdk (cargo new + cargo add)
  - browser         → js-client-sdk (Playwright v1.59.1 + Chromium;
                      check.js loads file:///snippet/index.html in
                      headless Chrome and polls page text for the
                      success line)

All eleven validators (python from the prior slice + ten new ones)
were verified end-to-end against the real LD test environment except
rust (cargo cold-build is multi-minute; CI exercises it). Each
validator that surfaced a snippet bug got fixed via "fix on red" in
the corresponding snippet — see the parent commit's notes.
`.github/workflows/snippets-validate.yml` runs `snippets validate
--sdk=<id>` for every validator-bearing SDK as a parallel matrix
cell. fail-fast is disabled so every cell runs to completion.

Each cell uses `launchdarkly/gh-actions/actions/verify-hello-app@
verify-hello-app-v2.0.1` — the same action the hello-* repos use —
to assume the AWS role from `vars.AWS_ROLE_ARN` (a repo *variable*,
not a secret) via OIDC, fetch the LaunchDarkly Sandbox account
credential from Secrets Manager, and inject it as
LAUNCHDARKLY_SDK_KEY / LAUNCHDARKLY_CLIENT_SIDE_ID /
LAUNCHDARKLY_MOBILE_KEY based on each row's `key-type:` field
(server | client | mobile). LAUNCHDARKLY_FLAG_KEY=sample-feature is
exported in the `command:` block; the action only handles the
SDK-side credential. No static LD secrets live on this repo.

After the matrix finishes, a final `summary` job downloads each
cell's status + log artifact, writes a markdown table to
$GITHUB_STEP_SUMMARY listing each SDK / pass-or-fail / 3-line
excerpt of the failure log, and exits non-zero if any cell failed
so the PR check goes red.

Initial matrix covers the eleven SDKs with validators landed in
this PR (python from the parent branch + ten new). Additional rows
land as more validators get wired (haskell, erlang, lua, cpp, ios,
vue, android, flutter, react-native, react-client, dotnet-client
once a real mobile key is provisioned). See validators/languages/
for the current list.
The repo variable was provisioned as AWS_ROLE_ARN_EXAMPLES (scoped to
the examples sandbox) rather than the generic AWS_ROLE_ARN the
hello-* repos use. Update both the role_arn input and the header
comment.
The verify-hello-app action already pulls LAUNCHDARKLY_FLAG_KEY from
the SSM parameter /sdk/common/hello-apps/boolean-flag-key — that's
how every hello-* repo's CI gets the flag key without setting it in
its own workflow. Our `export LAUNCHDARKLY_FLAG_KEY=sample-feature`
in the command block was redundant and would silently override the
shared value if it ever changes (e.g. if the test environment
swaps the canonical flag key). Drop it; let the action manage it.
Three fixes from the first CI run on the validator matrix:

- rust validator: bump rust:1.83 → rust:1.85 base image. The
  transitive dep `time-macros 0.2.27` requires the `edition2024`
  cargo feature, only stabilized in 1.85.

- js-client-sdk: context key was `'example-context-key'`. Every other
  hello-app uses `'example-user-key'`, and the test environment's
  `hello-boolean` flag is configured to evaluate to true for that
  user. The mismatched key was a snippet-author idiosyncrasy in
  gonfalon, not a deliberate divergence — normalize.

- dotnet-client-sdk: same root cause — `Context.New("context-key-
  123abc")` swapped for `Context.New("example-user-key")`. Validator
  observed the flag evaluating to False against `context-key-123abc`
  in the shared test env.

The other 8 cells (java, python, dotnet-server, php, node-client,
go, node-server, ruby) all green on this run.
LaunchDarkly already uses 'tier' to classify SDKs (the SDK tier list);
re-using T1/T2/T3/T4 in our validator matrix to mean 'Linux Docker'
vs 'browser' vs 'mobile' vs 'macOS' creates a name collision that
will mislead anyone skimming the workflow. Group the matrix rows by
plain runtime descriptor instead.
Pre-clones cpp-sdks at launchdarkly-cpp-server-v3.10.1 and prewarms ccache
by building the SDK once at image-build time, so per-validate cycles only
compile the user's main.cpp. Snippet's print line was the legacy
"Feature flag '<key>' is true" pattern; updated to the EXAM-HELLO
canonical form so await_success_line matches.
Same shape as the cpp-server validator: pre-clones cpp-sdks at
launchdarkly-cpp-client-v3.11.1, prewarms ccache by building the
client-SDK target once. Snippet's print line was the legacy
"Feature flag '<key>' is true" pattern; updated to the EXAM-HELLO
canonical.
The Lua SDK is a thin wrapper around the C++ Server SDK's C binding, so
the validator image builds cpp-sdks (launchdarkly-cpp-server-v3.10.1) as
a shared library at image-build time and luarocks-installs the wrapper
against it. Per-validate is just `lua hello.lua` against the staged
snippet. Snippet's print line was the legacy
"Feature flag '<key>' is <bool>" pattern; updated to EXAM-HELLO canonical.
Fixes a long-standing bug in the snippet (carried over from gonfalon):
the program called lookupEnv "{{ featureKey }}", which renders as
lookupEnv "sample-feature" — looking up an env var literally named after
the flag, which always failed at runtime. The Nothing branch then
defaulted back to "sample-feature", so the program worked by coincidence.
Hello-haskell-server uses lookupEnv "LAUNCHDARKLY_FLAG_KEY"; aligning
the snippet with the same pattern.

Validator uses haskell:9.6.7-bullseye with a pre-compiled cabal project
pinning launchdarkly-server-sdk-4.5.1 (latest on Hackage). The snippet's
stack.yaml/package.yaml fragments are still authored for ld-application
rendering but aren't exercised by the validator — the equivalent cabal
project is baked into the image.
Validator builds on the playwright base image: pre-bakes a Vue 3 + Vite
project with launchdarkly-vue-client-sdk@2.5.0, vue@3.5.33, vite@8.0.10
installed and warmed. Per-validate stages the snippet's src/main.js and
src/App.vue, runs vite build, starts vite preview, and points headless
Chromium at it.

Snippet's canonical line was "Feature Flag {{ featureKey }} is
{{ flagValue }}", which doesn't match the EXAM-HELLO success regex.
Updated to "The {{ featureKey }} feature flag evaluates to
{{ flagValue }}." — Vue's runtime mustaches survive the snippet
template engine via foreign-template pass-through.
CI's hello-boolean flag targets contexts whose user key is
example-user-key; the vue snippet's main.js used example-user, so the
flag returned its default (false) in CI. All other validated client
SDKs (node-client, dotnet-client, ios, android, react-native, js-client)
already use example-user-key. Cleaning up the rendered comment marker
on the snippet at the same time.
Engine: extend the template language with `{{ name | filter }}`. Today
only `camelCase` is supported. For ld-application this renders as
`${filter(name)}` (matching gonfalon's existing camelCase use); for
validation the filter is applied to the env-var value (`sample-feature`
becomes `sampleFeature`). Undeclared names pass through with the filter
preserved, so foreign templates aren't disturbed.

This unblocks react-client-sdk's snippets, where useFlags() destructures
camelCased identifiers from kebab-cased flag keys: the snippet body now
uses `{{ featureKey | camelCase }}`. Updated app-tsx and legacy-app-tsx
accordingly.

Validator: Vite + React + launchdarkly-react-client-sdk@3.9.0 image with
the bundle pre-warmed. Per-validate stages App.tsx + the entrypoint
(index.tsx for legacy, main.tsx for createApp), rewrites index.html to
point at it, builds, serves vite preview, and probes the DOM with
Playwright.

Wired the legacy variant: legacy-app-tsx now declares validation.runtime
+ companions, and gonfalon's legacy.tsx drops its hand-authored
"camelCase filter pending" Snippet in favor of the marker-driven render
(byte-equivalent JSX, just template-literal-formatted differently).
The createApp variant stays unwired — it needs gonfalon's createApp.tsx
to drop its assetSource/replaceAll pattern, plus the snippet's
<LDProvider> needs an explicit context= for the hello-app's user key.
Schema: extend ld-application with a `get-started-files: [list]` field
alongside the existing `get-started-file: string`, so a single SDK can
target multiple consumer files. react-client-sdk now renders into both
gonfalon variants (legacy.tsx + createApp.tsx).

Gonfalon: refactor createApp.tsx off its assetSource `?raw + replaceAll`
pattern. The snippet system can't intercept that — replacing it with
inline template literals so the markers do their normal job. The
assetSource/main.tsx.txt and assetSource/App.tsx.txt files are no
longer imported but left in place to keep this PR focused on the
createApp.tsx behavior.

Snippet: createApp's main-tsx was missing explicit context= on
<LDProvider>, so the SDK evaluated against an anonymous user and
hello-boolean returned its default. Added the standard
example-user-key context, matching the legacy variant + every other
client SDK.

Validator: same react-client runtime as legacy. With both variants now
declared validation.runtime, the CI cell exercises both in sequence
within the react-client-sdk job.
Validator runs Flutter's web build target — the only Linux-native path
for an SDK whose other targets need iOS/Android tooling. The Dockerfile
extends the playwright base with the Flutter SDK at /opt/flutter,
pre-creates a hello_flutter project, pins
launchdarkly_flutter_client_sdk@4.16.0 + provider@6.1.2, and pre-warms
`flutter build web` so per-validate cycles only recompile main.dart.

Per-validate: stage lib/main.dart, run `flutter build web` with
`--dart-define LAUNCHDARKLY_CLIENT_SIDE_ID=...` so the snippet's
CredentialSource.fromEnvironment() picks the credential up at
compile time, static-serve build/web with python's http.server,
probe the DOM with Playwright. The harness clicks Flutter's
<flt-semantics-placeholder> to force the semantics tree to populate
text into the DOM where Playwright can read it.

Snippet itself is a clean port — gonfalon's main.dart already prints
the canonical EXAM-HELLO line via Dart string interpolation.
The Get Started flow is interactive (rebar3 shell + manual
gen_server:call), so the snippet itself never prints the canonical
EXAM-HELLO line. Synthesize the equivalent at validate time: pre-bake
a hello_erlang OTP application with rebar.config + app.src +
supervisor pinned to launchdarkly_server_sdk@3.9.0, drop in the
snippet's hello_erlang_server.erl per-validate, run `rebar3 compile`,
and drive `erl -noshell -eval` to ensure_all_started + sleep +
hello_erlang_server:get/3 + io:format the canonical line + init:stop.

`rebar3 eval` doesn't exist as a built-in task on the bundled image,
so we drive `erl` directly with -pa pointing at the compiled beam
dirs under _build/default/lib/*/ebin.
Skip the Android emulator entirely: Robolectric runs the activity
lifecycle in the JVM with stubbed Android framework + real
OkHttp/network, which is enough to exercise the snippet's
init+evaluate path against the live LaunchDarkly streaming API.

Dockerfile clones hello-android (LD's official Get Started reference)
for project scaffolding, patches app/build.gradle to pin
launchdarkly-android-client-sdk@5.11.1 + Robolectric 4.12.2, and adds
a HelloAppTest that:
  1. Triggers MainApplication.onCreate via ApplicationProvider so
     LDClient.init runs with the snippet-baked mobile key.
  2. Drives MainActivity through Robolectric's lifecycle controller.
  3. Polls textView.text for the canonical EXAM-HELLO line, flushing
     the foreground looper between checks so the streaming SDK's flag
     listener can fire.

Per-validate is just `gradlew testDebugUnitTest`. Total local time
(warm gradle cache): ~45s.
Skip Metro + iOS simulator + Android emulator entirely: jest with
@react-native/jest-preset + @testing-library/react-native renders the
snippet's component tree in a pure node environment, which is enough
to exercise the SDK's init+evaluate path against the live LaunchDarkly
backend.

Two adjustments versus a vanilla setup:

1. AppState.currentState is undefined in the jest env; the SDK's
   RNStateDetector translates that to ApplicationState.Background, and
   with automaticBackgroundHandling+runInBackground=false (the SDK
   defaults), the connection manager immediately switches to offline
   mode. jest.setup.js pins AppState.currentState = 'active'.

2. The SDK's streaming transport (RNEventSource, internal in
   @launchdarkly/react-native-client-sdk) is implemented with
   XMLHttpRequest, which doesn't exist in Node. The test mocks
   ReactNativeLDClient via a subclass that forces
   initialConnectionMode='polling' so the SDK uses fetch (built into
   Node 18+) instead.

Per-validate stages App.tsx + src/welcome.tsx and runs jest. ~30s on a
warm cache (mostly polling roundtrip + jest startup).
@kinyoklion kinyoklion force-pushed the rlamb/port-remaining-getting-started branch from 0d5c3a8 to 496c7ed Compare April 28, 2026 20:41
Validator runs on macos-latest with xcodebuild against an iPhone
simulator. The Xcode project is generated at validate time via
xcodegen from a yaml spec checked into the validator scaffold; the
launchdarkly-ios-client-sdk dependency comes in via Swift Package
Manager (avoiding the CocoaPods step the snippet shows the user — we're
testing the snippet's runtime behavior, not the dependency manager).

The harness drops the snippet's AppDelegate.swift + ViewController.swift
into Sources/, runs xcodegen to materialize HelloIOS.xcodeproj, and
runs `xcodebuild test`. The XCTest case manually wires a UILabel into
the IBOutlet (no storyboard), drives viewDidLoad, observes the flag,
and asserts the rendered text contains the canonical EXAM-HELLO line.

Two snippet fixes also landed (fix-on-red against the now-running
validator):
- ViewController.swift: `(flagKey)` → `\(flagKey)` and `(result)` →
  `\(result)`. The original had broken Swift string interpolation,
  printing the literal token text. Carried verbatim from gonfalon.
- Podfile fallback bumped from `'6.1.0'` (4 majors stale) to `'11.1.2'`.

`mode: native` because xcodebuild + iOS Simulator can't run inside
Linux containers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants